﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="InstallerManager.cs" company="Anton Frattaroli">
//   Copyright (c) Anton Frattaroli. All rights reserved.
// </copyright>
// <summary>
//   Defines the InstallerManager class.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace Lidocaine.BusinessLogic
{
    using System;
    using System.Configuration;
    using System.Globalization;
    using System.IO;
    using System.Resources;
    using System.Security.Cryptography;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Web;
    using System.Xml;
    using Properties;
    
    /// <summary>
    /// Defines the InstallerManager class.
    /// </summary>
    public static class InstallerManager
    {
        /// <summary>
        /// Installs the underlying database structure and adjusts the web.config file accordingly.
        /// </summary>
        /// <param name="siteTitle">The title of the website.</param>
        /// <param name="siteDomain">The DNS domain of the website.</param>
        /// <param name="useAdmin">Whether or not to use the administrator account to access the SQL database.</param>
        /// <param name="userName">The database owner username.</param>
        /// <param name="userPassword">The database owner password.</param>
        /// <param name="databaseName">The database name.</param>
        /// <param name="databaseServer">The database server.</param>
        public static void Install(string siteTitle, string siteDomain, string useAdmin, string userName, string userPassword, string databaseName, string databaseServer)
        {
            if (ConfigurationManager.ConnectionStrings["Lidocaine"] == null)
            {
                try
                {
                    HttpCookie decoded = new HttpCookie("Lidocaine");
                    decoded.Values["ID"] = "0";
                    decoded.Values["Expires"] = DateTime.Now.AddDays(-1).ToString(CultureInfo.InvariantCulture);
                    decoded.Expires = DateTime.Now.AddDays(-1);
                    decoded.Domain = siteDomain;
                    HttpCookie encoded = HttpSecureCookie.Encode(decoded);
                    HttpContext.Current.Response.Cookies.Set(encoded);

                    string databaseUsername, databasePassword;
                    if (useAdmin == "1")
                    {
                        databaseUsername = userName;
                        databasePassword = userPassword;
                        Data.Installer.Install(true, databaseUsername, databasePassword, databaseName, databaseServer, null, null);
                    }
                    else
                    {
                        databaseUsername = InstallerManager.GetRandomString(8);
                        databasePassword = InstallerManager.GetRandomString(10);
                        Data.Installer.Install(false, userName, userPassword, databaseName, databaseServer, databaseUsername, databasePassword);
                    }
                    
                    XmlDocument existingConfig = new XmlDocument();
                    existingConfig.LoadXml(Web_default_config.Web_default);

                    // Lidocaine appConfig
                    XmlNode appConfig = existingConfig.SelectSingleNode("/configuration/lidocaine/appConfig");
                    appConfig.Attributes["title"].Value = siteTitle;
                    appConfig.Attributes["domain"].Value = siteDomain;
                    appConfig.ParentNode.ReplaceChild(appConfig, existingConfig.SelectSingleNode("/configuration/lidocaine/appConfig"));

                    // Connection String
                    XmlNode lidocaineConnectionString = existingConfig.SelectSingleNode("/configuration/connectionStrings/add");
                    lidocaineConnectionString.Attributes["connectionString"].Value = string.Format(CultureInfo.InvariantCulture, "Data Source={0};Initial Catalog={1};User ID={2};Password={3};", new object[] { databaseServer, databaseName, databaseUsername, databasePassword });
                    lidocaineConnectionString.ParentNode.ReplaceChild(lidocaineConnectionString, existingConfig.SelectSingleNode("/configuration/connectionStrings/add"));

                    // Machine Key
                    XmlNode machineKey = existingConfig.SelectSingleNode("/configuration/system.web/machineKey");
                    machineKey.Attributes["validationKey"].Value = InstallerManager.GetRandomKey(64);
                    machineKey.Attributes["decryptionKey"].Value = InstallerManager.GetRandomKey(32);
                    machineKey.ParentNode.ReplaceChild(machineKey, existingConfig.SelectSingleNode("/configuration/system.web/machineKey"));

                    using (StreamWriter sw = new StreamWriter(HttpContext.Current.Server.MapPath("~/") + "web.config"))
                    {
                        existingConfig.Save(sw);
                    }
                }
                catch (Exception ex)
                {
                    HttpContext.Current.Response.StatusCode = 500;
                    HttpContext.Current.Response.Write("An error occurred during installation: " + ex.Message + "<br />" + ex.InnerException);
                }
            }
            else
            {
                HttpContext.Current.Response.StatusCode = 500;
                HttpContext.Current.Response.Write("Database installation failed: Connection string named \"Lidocaine\" already exists.");
            }
        }

        public static BusinessObjects.UserProfile Setup(string siteDomain)
        {
            return UserProfileManager.GetUserProfile(Data.Installer.Setup(siteDomain));
        }

        /// <summary>
        /// Generates a random key of a certain byte length for the purpose of setting up the MachineKey configuration section.
        /// </summary>
        /// <param name="bytelength">The number of bytes returned (usu. 32 or 64).</param>
        /// <returns>The generated string.</returns>
        private static string GetRandomKey(int bytelength)
        {
            byte[] buff = new byte[bytelength];
            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
            rng.GetBytes(buff);
            StringBuilder sb = new StringBuilder(bytelength * 2);
            for (int i = 0; i < buff.Length; i++)
            {
                sb.Append(string.Format(CultureInfo.InvariantCulture, "{0:X2}", buff[i]));
            }

            return sb.ToString();
        }

        /// <summary>
        /// Generates a string of random unambiguous alphanumeric characters.
        /// </summary>
        /// <param name="length">The desired length of the returned string.</param>
        /// <returns>The generated string.</returns>
        private static string GetRandomString(int length)
        {
            char[][] charGroups = new char[][] { "abcdefgijkmnopqrstwxyz".ToCharArray(), "ABCDEFGHJKLMNPQRSTWXYZ".ToCharArray(), "23456789".ToCharArray() };
            int[] charsLeftInGroup = new int[charGroups.Length];
            for (int i = 0; i < charsLeftInGroup.Length; i++)
            {
                charsLeftInGroup[i] = charGroups[i].Length;
            }

            int[] leftGroupsOrder = new int[charGroups.Length];
            for (int i = 0; i < leftGroupsOrder.Length; i++)
            {
                leftGroupsOrder[i] = i;
            }

            byte[] randomBytes = new byte[4];
            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
            rng.GetBytes(randomBytes);
            int seed = (randomBytes[0] & 0x7f) << 24 | randomBytes[1] << 16 | randomBytes[2] << 8 | randomBytes[3];
            Random random = new Random(seed);
            char[] password = null;
            password = new char[length];
            int nextCharIdx;
            int nextGroupIdx;
            int nextLeftGroupsOrderIdx;
            int lastCharIdx;
            int lastLeftGroupsOrderIdx = leftGroupsOrder.Length - 1;
            for (int i = 0; i < password.Length; i++)
            {
                if (lastLeftGroupsOrderIdx == 0)
                {
                    nextLeftGroupsOrderIdx = 0;
                }
                else
                {
                    nextLeftGroupsOrderIdx = random.Next(0, lastLeftGroupsOrderIdx);
                }

                nextGroupIdx = leftGroupsOrder[nextLeftGroupsOrderIdx];
                lastCharIdx = charsLeftInGroup[nextGroupIdx] - 1;
                if (lastCharIdx == 0)
                {
                    nextCharIdx = 0;
                }
                else
                {
                    nextCharIdx = random.Next(0, lastCharIdx + 1);
                }

                password[i] = charGroups[nextGroupIdx][nextCharIdx];
                if (lastCharIdx == 0)
                {
                    charsLeftInGroup[nextGroupIdx] = charGroups[nextGroupIdx].Length;
                }
                else
                {
                    if (lastCharIdx != nextCharIdx)
                    {
                        char temp = charGroups[nextGroupIdx][lastCharIdx];
                        charGroups[nextGroupIdx][lastCharIdx] = charGroups[nextGroupIdx][nextCharIdx];
                        charGroups[nextGroupIdx][nextCharIdx] = temp;
                    }

                    charsLeftInGroup[nextGroupIdx]--;
                }

                if (lastLeftGroupsOrderIdx == 0)
                {
                    lastLeftGroupsOrderIdx = leftGroupsOrder.Length - 1;
                }
                else
                {
                    if (lastLeftGroupsOrderIdx != nextLeftGroupsOrderIdx)
                    {
                        int temp = leftGroupsOrder[lastLeftGroupsOrderIdx];
                        leftGroupsOrder[lastLeftGroupsOrderIdx] = leftGroupsOrder[nextLeftGroupsOrderIdx];
                        leftGroupsOrder[nextLeftGroupsOrderIdx] = temp;
                    }

                    lastLeftGroupsOrderIdx--;
                }
            }

            string strPassword = new string(password);
            return strPassword;
        }
    }
}